Wallet.unlock   A
last analyzed

Complexity

Conditions 1
Paths 2

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
nc 2
dl 0
loc 54
rs 9.6716
nop 2

4 Functions

Rating   Name   Duplication   Size   Complexity  
A 0 3 1
A 0 3 1
A 0 19 4
B 0 15 5

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
var _ = require('lodash');
2
var assert = require('assert');
3
var q = require('q');
4
var async = require('async');
5
var bitcoin = require('bitcoinjs-lib');
6
var bitcoinMessage = require('bitcoinjs-message');
7
var blocktrail = require('./blocktrail');
8
var CryptoJS = require('crypto-js');
9
var Encryption = require('./encryption');
10
var EncryptionMnemonic = require('./encryption_mnemonic');
11
var SizeEstimation = require('./size_estimation');
12
var bip39 = require('bip39');
13
14
var SignMode = {
15
    SIGN: "sign",
16
    DONT_SIGN: "dont_sign"
17
};
18
19
/**
20
 *
21
 * @param sdk                   APIClient       SDK instance used to do requests
22
 * @param identifier            string          identifier of the wallet
23
 * @param walletVersion         string
24
 * @param primaryMnemonic       string          primary mnemonic
25
 * @param encryptedPrimarySeed
26
 * @param encryptedSecret
27
 * @param primaryPublicKeys     string          primary mnemonic
28
 * @param backupPublicKey       string          BIP32 master pubKey M/
29
 * @param blocktrailPublicKeys  array           list of blocktrail pubKeys indexed by keyIndex
30
 * @param keyIndex              int             key index to use
31
 * @param segwit                int             segwit toggle from server
32
 * @param testnet               bool            testnet
33
 * @param regtest               bool            regtest
34
 * @param checksum              string
35
 * @param upgradeToKeyIndex     int
36
 * @param useNewCashAddr        bool            flag to opt in to bitcoin cash cashaddr's
37
 * @param bypassNewAddressCheck bool            flag to indicate if wallet should/shouldn't derive new address locally to verify api
38
 * @constructor
39
 * @internal
40
 */
41
var Wallet = function(
42
    sdk,
43
    identifier,
44
    walletVersion,
45
    primaryMnemonic,
46
    encryptedPrimarySeed,
47
    encryptedSecret,
48
    primaryPublicKeys,
49
    backupPublicKey,
50
    blocktrailPublicKeys,
51
    keyIndex,
52
    segwit,
53
    testnet,
54
    regtest,
55
    checksum,
56
    upgradeToKeyIndex,
57
    useNewCashAddr,
58
    bypassNewAddressCheck
59
) {
60
    /* jshint -W071 */
61
    var self = this;
62
63
    self.sdk = sdk;
64
    self.identifier = identifier;
65
    self.walletVersion = walletVersion;
66
    self.locked = true;
67
    self.bypassNewAddressCheck = !!bypassNewAddressCheck;
68
    self.bitcoinCash = self.sdk.bitcoinCash;
69
    self.segwit = !!segwit;
70
    self.useNewCashAddr = !!useNewCashAddr;
71
    assert(!self.segwit || !self.bitcoinCash);
72
73
    self.testnet = testnet;
74
    self.regtest = regtest;
75
    if (self.bitcoinCash) {
76
        if (self.regtest) {
77
            self.network = bitcoin.networks.bitcoincashregtest;
78
        } else if (self.testnet) {
79
            self.network = bitcoin.networks.bitcoincashtestnet;
80
        } else {
81
            self.network = bitcoin.networks.bitcoincash;
82
        }
83
    } else {
84
        if (self.regtest) {
85
            self.network = bitcoin.networks.regtest;
86
        } else if (self.testnet) {
87
            self.network = bitcoin.networks.testnet;
88
        } else {
89
            self.network = bitcoin.networks.bitcoin;
90
        }
91
    }
92
93
    assert(backupPublicKey instanceof bitcoin.HDNode);
94
    assert(_.every(primaryPublicKeys, function(primaryPublicKey) { return primaryPublicKey instanceof bitcoin.HDNode; }));
95
    assert(_.every(blocktrailPublicKeys, function(blocktrailPublicKey) { return blocktrailPublicKey instanceof bitcoin.HDNode; }));
96
97
    // v1
98
    self.primaryMnemonic = primaryMnemonic;
99
100
    // v2 & v3
101
    self.encryptedPrimarySeed = encryptedPrimarySeed;
102
    self.encryptedSecret = encryptedSecret;
103
104
    self.primaryPrivateKey = null;
105
    self.backupPrivateKey = null;
106
107
    self.backupPublicKey = backupPublicKey;
108
    self.blocktrailPublicKeys = blocktrailPublicKeys;
109
    self.primaryPublicKeys = primaryPublicKeys;
110
    self.keyIndex = keyIndex;
111
112
    if (!self.bitcoinCash) {
113
        if (self.segwit) {
114
            self.chain = Wallet.CHAIN_BTC_DEFAULT;
115
            self.changeChain = Wallet.CHAIN_BTC_SEGWIT;
116
        } else {
117
            self.chain = Wallet.CHAIN_BTC_DEFAULT;
118
            self.changeChain = Wallet.CHAIN_BTC_DEFAULT;
119
        }
120
    } else {
121
        self.chain = Wallet.CHAIN_BCC_DEFAULT;
122
        self.changeChain = Wallet.CHAIN_BCC_DEFAULT;
123
    }
124
125
    self.checksum = checksum;
126
    self.upgradeToKeyIndex = upgradeToKeyIndex;
127
128
    self.secret = null;
129
    self.seedHex = null;
130
};
131
132
Wallet.WALLET_VERSION_V1 = 'v1';
133
Wallet.WALLET_VERSION_V2 = 'v2';
134
Wallet.WALLET_VERSION_V3 = 'v3';
135
136
Wallet.WALLET_ENTROPY_BITS = 256;
137
138
Wallet.OP_RETURN = 'opreturn';
139
Wallet.DATA = Wallet.OP_RETURN; // alias
140
141
Wallet.PAY_PROGRESS_START = 0;
142
Wallet.PAY_PROGRESS_COIN_SELECTION = 10;
143
Wallet.PAY_PROGRESS_CHANGE_ADDRESS = 20;
144
Wallet.PAY_PROGRESS_SIGN = 30;
145
Wallet.PAY_PROGRESS_SEND = 40;
146
Wallet.PAY_PROGRESS_DONE = 100;
147
148
Wallet.CHAIN_BTC_DEFAULT = 0;
149
Wallet.CHAIN_BTC_SEGWIT = 2;
150
Wallet.CHAIN_BCC_DEFAULT = 1;
151
152
Wallet.FEE_STRATEGY_FORCE_FEE = blocktrail.FEE_STRATEGY_FORCE_FEE;
153
Wallet.FEE_STRATEGY_BASE_FEE = blocktrail.FEE_STRATEGY_BASE_FEE;
154
Wallet.FEE_STRATEGY_HIGH_PRIORITY = blocktrail.FEE_STRATEGY_HIGH_PRIORITY;
155
Wallet.FEE_STRATEGY_OPTIMAL = blocktrail.FEE_STRATEGY_OPTIMAL;
156
Wallet.FEE_STRATEGY_LOW_PRIORITY = blocktrail.FEE_STRATEGY_LOW_PRIORITY;
157
Wallet.FEE_STRATEGY_MIN_RELAY_FEE = blocktrail.FEE_STRATEGY_MIN_RELAY_FEE;
158
159
Wallet.prototype.isSegwit = function() {
160
    return !!this.segwit;
161
};
162
163
Wallet.prototype.unlock = function(options, cb) {
164
    var self = this;
165
166
    var deferred = q.defer();
167
    deferred.promise.nodeify(cb);
168
169
    // avoid modifying passed options
170
    options = _.merge({}, options);
171
172
    q.fcall(function() {
173
        switch (self.walletVersion) {
174
            case Wallet.WALLET_VERSION_V1:
0 ignored issues
show
Bug introduced by
The variable Wallet seems to be never declared. If this is a global, consider adding a /** global: Wallet */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
175
                return self.unlockV1(options);
176
177
            case Wallet.WALLET_VERSION_V2:
178
                return self.unlockV2(options);
179
180
            case Wallet.WALLET_VERSION_V3:
181
                return self.unlockV3(options);
182
183
            default:
184
                return q.reject(new blocktrail.WalletInitError("Invalid wallet version"));
185
        }
186
    }).then(
187
        function(primaryPrivateKey) {
188
            self.primaryPrivateKey = primaryPrivateKey;
189
190
            // create a checksum of our private key which we'll later use to verify we used the right password
191
            var checksum = self.primaryPrivateKey.getAddress();
192
193
            // check if we've used the right passphrase
194
            if (checksum !== self.checksum) {
195
                throw new blocktrail.WalletChecksumError("Generated checksum [" + checksum + "] does not match " +
196
                    "[" + self.checksum + "], most likely due to incorrect password");
197
            }
198
199
            self.locked = false;
200
201
            // if the response suggests we should upgrade to a different blocktrail cosigning key then we should
202
            if (typeof self.upgradeToKeyIndex !== "undefined" && self.upgradeToKeyIndex !== null) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if typeof self.upgradeToKey...radeToKeyIndex !== null is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
203
                return self.upgradeKeyIndex(self.upgradeToKeyIndex);
204
            }
205
        }
206
    ).then(
207
        function(r) {
208
            deferred.resolve(r);
209
        },
210
        function(e) {
211
            deferred.reject(e);
212
        }
213
    );
214
215
    return deferred.promise;
216
};
217
218
Wallet.prototype.unlockV1 = function(options) {
219
    var self = this;
220
221
    options.primaryMnemonic = typeof options.primaryMnemonic !== "undefined" ? options.primaryMnemonic : self.primaryMnemonic;
222
    options.secretMnemonic = typeof options.secretMnemonic !== "undefined" ? options.secretMnemonic : self.secretMnemonic;
223
224
    return self.sdk.resolvePrimaryPrivateKeyFromOptions(options)
225
        .then(function(options) {
226
            self.primarySeed = options.primarySeed;
227
228
            return options.primaryPrivateKey;
229
        });
230
};
231
232
Wallet.prototype.unlockV2 = function(options, cb) {
233
    var self = this;
234
235
    var deferred = q.defer();
236
    deferred.promise.nodeify(cb);
237
238
    deferred.resolve(q.fcall(function() {
239
        /* jshint -W071, -W074 */
240
        options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
241
        options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
242
243
        if (options.secret) {
244
            self.secret = options.secret;
245
        }
246
247
        if (options.primaryPrivateKey) {
248
            throw new blocktrail.WalletDecryptError("specifying primaryPrivateKey has been deprecated");
249
        }
250
251
        if (options.primarySeed) {
252
            self.primarySeed = options.primarySeed;
253
        } else if (options.secret) {
254
            try {
255
                self.primarySeed = new Buffer(
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
256
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
257
                if (!self.primarySeed.length) {
258
                    throw new Error();
259
                }
260
            } catch (e) {
261
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
262
            }
263
264
        } else {
265
            // avoid conflicting options
266
            if (options.passphrase && options.password) {
267
                throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
268
            }
269
            // normalize passphrase/password
270
            options.passphrase = options.passphrase || options.password;
271
272
            try {
273
                self.secret = CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedSecret), options.passphrase).toString(CryptoJS.enc.Utf8);
274
                if (!self.secret.length) {
275
                    throw new Error();
276
                }
277
            } catch (e) {
278
                throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
279
            }
280
            try {
281
                self.primarySeed = new Buffer(
282
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
283
                if (!self.primarySeed.length) {
284
                    throw new Error();
285
                }
286
            } catch (e) {
287
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
288
            }
289
        }
290
291
        return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
292
    }));
293
294
    return deferred.promise;
295
};
296
297
Wallet.prototype.unlockV3 = function(options, cb) {
298
    var self = this;
299
300
    var deferred = q.defer();
301
    deferred.promise.nodeify(cb);
302
303
    deferred.resolve(q.fcall(function() {
304
        return q.when()
305
            .then(function() {
306
                /* jshint -W071, -W074 */
307
                options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
308
                options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
309
310
                if (options.secret) {
311
                    self.secret = options.secret;
312
                }
313
314
                if (options.primaryPrivateKey) {
315
                    throw new blocktrail.WalletInitError("specifying primaryPrivateKey has been deprecated");
316
                }
317
318
                if (options.primarySeed) {
319
                    self.primarySeed = options.primarySeed;
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
320
                } else if (options.secret) {
321
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
322
                        .then(function(primarySeed) {
323
                            self.primarySeed = primarySeed;
324
                        }, function() {
325
                            throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
326
                        });
327
                } else {
328
                    // avoid conflicting options
329
                    if (options.passphrase && options.password) {
330
                        throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
331
                    }
332
                    // normalize passphrase/password
333
                    options.passphrase = options.passphrase || options.password;
334
                    delete options.password;
335
336
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedSecret, 'base64'), new Buffer(options.passphrase))
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
337
                        .then(function(secret) {
338
                            self.secret = secret;
339
                        }, function() {
340
                            throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
341
                        })
342
                        .then(function() {
343
                            return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
344
                                .then(function(primarySeed) {
345
                                    self.primarySeed = primarySeed;
346
                                }, function() {
347
                                    throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
348
                                });
349
                        });
350
                }
351
            })
352
            .then(function() {
353
                return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
354
            })
355
        ;
356
    }));
357
358
    return deferred.promise;
359
};
360
361
Wallet.prototype.lock = function() {
362
    var self = this;
363
364
    self.secret = null;
365
    self.primarySeed = null;
366
    self.primaryPrivateKey = null;
367
    self.backupPrivateKey = null;
368
369
    self.locked = true;
370
};
371
372
/**
373
 * upgrade wallet to V3 encryption scheme
374
 *
375
 * @param passphrase is required again to reencrypt the data, important that it's the correct password!!!
376
 * @param cb
377
 * @returns {promise}
378
 */
379
Wallet.prototype.upgradeToV3 = function(passphrase, cb) {
380
    var self = this;
381
382
    var deferred = q.defer();
383
    deferred.promise.nodeify(cb);
384
385
    q.when(true)
386
        .then(function() {
387
            if (self.locked) {
388
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade");
389
            }
390
391
            if (self.walletVersion === Wallet.WALLET_VERSION_V3) {
392
                throw new blocktrail.WalletUpgradeError("Wallet is already V3");
393
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
394
                return self._upgradeV2ToV3(passphrase, deferred.notify.bind(deferred));
395
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if self.walletVersion === Wallet.WALLET_VERSION_V1 is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
396
                return self._upgradeV1ToV3(passphrase, deferred.notify.bind(deferred));
397
            }
398
        })
399
        .then(function(r) { deferred.resolve(r); }, function(e) { deferred.reject(e); });
400
401
    return deferred.promise;
402
};
403
404
Wallet.prototype._upgradeV2ToV3 = function(passphrase, notify) {
405
    var self = this;
406
407
    return q.when(true)
408
        .then(function() {
409
            var options = {
410
                storeDataOnServer: true,
411
                passphrase: passphrase,
412
                primarySeed: self.primarySeed,
413
                recoverySecret: false // don't create new recovery secret, V2 already has ones
414
            };
415
416
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
417
                .then(function(options) {
418
                    return self.sdk.updateWallet(self.identifier, {
419
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
420
                        encrypted_secret: options.encryptedSecret.toString('base64'),
421
                        wallet_version: Wallet.WALLET_VERSION_V3
422
                    }).then(function() {
423
                        self.secret = options.secret;
424
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
425
                        self.encryptedSecret = options.encryptedSecret;
426
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
427
428
                        return self;
429
                    });
430
                });
431
        });
432
433
};
434
435
Wallet.prototype._upgradeV1ToV3 = function(passphrase, notify) {
436
    var self = this;
437
438
    return q.when(true)
439
        .then(function() {
440
            var options = {
441
                storeDataOnServer: true,
442
                passphrase: passphrase,
443
                primarySeed: self.primarySeed
444
            };
445
446
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
447
                .then(function(options) {
448
                    // store recoveryEncryptedSecret for printing on backup sheet
449
                    self.recoveryEncryptedSecret = options.recoveryEncryptedSecret;
450
451
                    return self.sdk.updateWallet(self.identifier, {
452
                        primary_mnemonic: '',
453
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
454
                        encrypted_secret: options.encryptedSecret.toString('base64'),
455
                        recovery_secret: options.recoverySecret.toString('hex'),
456
                        wallet_version: Wallet.WALLET_VERSION_V3
457
                    }).then(function() {
458
                        self.secret = options.secret;
459
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
460
                        self.encryptedSecret = options.encryptedSecret;
461
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
462
463
                        return self;
464
                    });
465
                });
466
        });
467
};
468
469
Wallet.prototype.doPasswordChange = function(newPassword) {
470
    var self = this;
471
472
    return q.when(null)
473
        .then(function() {
474
475
            if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
476
                throw new blocktrail.WalletLockedError("Wallet version does not support password change!");
477
            }
478
479
            if (self.locked) {
480
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to change password");
481
            }
482
483
            if (!self.secret) {
484
                throw new blocktrail.WalletLockedError("No secret");
485
            }
486
487
            var newEncryptedSecret;
488
            var newEncrypedWalletSecretMnemonic;
489
            if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
490
                newEncryptedSecret = CryptoJS.AES.encrypt(self.secret, newPassword).toString(CryptoJS.format.OpenSSL);
491
                newEncrypedWalletSecretMnemonic = bip39.entropyToMnemonic(blocktrail.convert(newEncryptedSecret, 'base64', 'hex'));
492
493
            } else {
494
                if (typeof newPassword === "string") {
495
                    newPassword = new Buffer(newPassword);
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
496
                } else {
497
                    if (!(newPassword instanceof Buffer)) {
498
                        throw new Error('New password must be provided as a string or a Buffer');
499
                    }
500
                }
501
502
                newEncryptedSecret = Encryption.encrypt(self.secret, newPassword);
503
                newEncrypedWalletSecretMnemonic = EncryptionMnemonic.encode(newEncryptedSecret);
504
505
                // It's a buffer, so convert it back to base64
506
                newEncryptedSecret = newEncryptedSecret.toString('base64');
507
            }
508
509
            return [newEncryptedSecret, newEncrypedWalletSecretMnemonic];
510
        });
511
};
512
513
Wallet.prototype.passwordChange = function(newPassword, cb) {
514
    var self = this;
515
516
    var deferred = q.defer();
517
    deferred.promise.nodeify(cb);
518
519
    q.fcall(function() {
520
        return self.doPasswordChange(newPassword)
521
            .then(function(r) {
522
                var newEncryptedSecret = r[0];
523
                var newEncrypedWalletSecretMnemonic = r[1];
524
525
                return self.sdk.updateWallet(self.identifier, {encrypted_secret: newEncryptedSecret}).then(function() {
526
                    self.encryptedSecret = newEncryptedSecret;
527
528
                    // backupInfo
529
                    return {
530
                        encryptedSecret: newEncrypedWalletSecretMnemonic
531
                    };
532
                });
533
            })
534
            .then(
535
                function(r) {
536
                    deferred.resolve(r);
537
                },
538
                function(e) {
539
                    deferred.reject(e);
540
                }
541
            );
542
    });
543
544
    return deferred.promise;
545
};
546
547
/**
548
 * get address for specified path
549
 *
550
 * @param path
551
 * @returns string
552
 */
553
Wallet.prototype.getAddressByPath = function(path) {
554
    return this.getWalletScriptByPath(path).address;
555
};
556
557
/**
558
 * get redeemscript for specified path
559
 *
560
 * @param path
561
 * @returns {Buffer}
562
 */
563
Wallet.prototype.getRedeemScriptByPath = function(path) {
564
    return this.getWalletScriptByPath(path).redeemScript;
565
};
566
567
/**
568
 * Generate scripts, and address.
569
 * @param path
570
 * @returns {{witnessScript: *, redeemScript: *, scriptPubKey, address: *}}
571
 */
572
Wallet.prototype.getWalletScriptByPath = function(path) {
573
    var self = this;
574
575
    // get derived primary key
576
    var derivedPrimaryPublicKey = self.getPrimaryPublicKey(path);
577
    // get derived blocktrail key
578
    var derivedBlocktrailPublicKey = self.getBlocktrailPublicKey(path);
579
    // derive the backup key
580
    var derivedBackupPublicKey = Wallet.deriveByPath(self.backupPublicKey, path.replace("'", ""), "M");
581
582
    // sort the pubkeys
583
    var pubKeys = Wallet.sortMultiSigKeys([
584
        derivedPrimaryPublicKey.keyPair.getPublicKeyBuffer(),
585
        derivedBackupPublicKey.keyPair.getPublicKeyBuffer(),
586
        derivedBlocktrailPublicKey.keyPair.getPublicKeyBuffer()
587
    ]);
588
589
    var multisig = bitcoin.script.multisig.output.encode(2, pubKeys);
590
    var scriptType = parseInt(path.split("/")[2]);
591
592
    var ws, rs;
593
    if (this.network !== "bitcoincash" && scriptType === Wallet.CHAIN_BTC_SEGWIT) {
594
        ws = multisig;
595
        rs = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(ws));
596
    } else {
597
        ws = null;
598
        rs = multisig;
599
    }
600
601
    var spk = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(rs));
602
    var addr = bitcoin.address.fromOutputScript(spk, this.network, self.useNewCashAddr);
603
604
    return {
605
        witnessScript: ws,
606
        redeemScript: rs,
607
        scriptPubKey: spk,
608
        address: addr
609
    };
610
};
611
612
/**
613
 * get primary public key by path
614
 *  first level of the path is used as keyIndex to find the correct key in the dict
615
 *
616
 * @param path  string
617
 * @returns {bitcoin.HDNode}
618
 */
619
Wallet.prototype.getPrimaryPublicKey = function(path) {
620
    var self = this;
621
622
    path = path.replace("m", "M");
623
624
    var keyIndex = path.split("/")[1].replace("'", "");
625
626
    if (!self.primaryPublicKeys[keyIndex]) {
627
        if (self.primaryPrivateKey) {
628
            self.primaryPublicKeys[keyIndex] = Wallet.deriveByPath(self.primaryPrivateKey, "M/" + keyIndex + "'", "m");
629
        } else {
630
            throw new blocktrail.KeyPathError("Wallet.getPrimaryPublicKey keyIndex (" + keyIndex + ") is unknown to us");
631
        }
632
    }
633
634
    var primaryPublicKey = self.primaryPublicKeys[keyIndex];
635
    return Wallet.deriveByPath(primaryPublicKey, path, "M/" + keyIndex + "'");
636
};
637
638
/**
639
 * get blocktrail public key by path
640
 *  first level of the path is used as keyIndex to find the correct key in the dict
641
 *
642
 * @param path  string
643
 * @returns {bitcoin.HDNode}
644
 */
645
Wallet.prototype.getBlocktrailPublicKey = function(path) {
646
    var self = this;
647
648
    path = path.replace("m", "M");
649
650
    var keyIndex = path.split("/")[1].replace("'", "");
651
652
    if (!self.blocktrailPublicKeys[keyIndex]) {
653
        throw new blocktrail.KeyPathError("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us");
654
    }
655
656
    var blocktrailPublicKey = self.blocktrailPublicKeys[keyIndex];
657
658
    return Wallet.deriveByPath(blocktrailPublicKey, path, "M/" + keyIndex + "'");
659
};
660
661
/**
662
 * upgrade wallet to different blocktrail cosign key
663
 *
664
 * @param keyIndex  int
665
 * @param [cb]      function
666
 * @returns {q.Promise}
667
 */
668
Wallet.prototype.upgradeKeyIndex = function(keyIndex, cb) {
669
    var self = this;
670
671
    var deferred = q.defer();
672
    deferred.promise.nodeify(cb);
673
674
    if (self.locked) {
675
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade key index"));
676
        return deferred.promise;
677
    }
678
679
    var primaryPublicKey = self.primaryPrivateKey.deriveHardened(keyIndex).neutered();
680
681
    deferred.resolve(
682
        self.sdk.upgradeKeyIndex(self.identifier, keyIndex, [primaryPublicKey.toBase58(), "M/" + keyIndex + "'"])
683
            .then(function(result) {
684
                self.keyIndex = keyIndex;
685
                _.forEach(result.blocktrail_public_keys, function(publicKey, keyIndex) {
686
                    self.blocktrailPublicKeys[keyIndex] = bitcoin.HDNode.fromBase58(publicKey[0], self.network);
687
                });
688
689
                self.primaryPublicKeys[keyIndex] = primaryPublicKey;
690
691
                return true;
692
            })
693
    );
694
695
    return deferred.promise;
696
};
697
698
/**
699
 * generate a new derived private key and return the new address for it
700
 *
701
 * @param [chainIdx] int
702
 * @param [cb]  function        callback(err, address)
703
 * @returns {q.Promise}
704
 */
705
Wallet.prototype.getNewAddress = function(chainIdx, cb) {
706
    var self = this;
707
708
    // chainIdx is optional
709
    if (typeof chainIdx === "function") {
710
        cb = chainIdx;
711
        chainIdx = null;
712
    }
713
714
    var deferred = q.defer();
715
    deferred.promise.spreadNodeify(cb);
716
717
    // Only enter if it's not an integer
718
    if (chainIdx !== parseInt(chainIdx, 10)) {
719
        // deal with undefined or null, assume defaults
720
        if (typeof chainIdx === "undefined" || chainIdx === null) {
721
            chainIdx = self.chain;
722
        } else {
723
            // was a variable but not integer
724
            deferred.reject(new Error("Invalid chain index"));
725
            return deferred.promise;
726
        }
727
    }
728
729
    deferred.resolve(
730
        self.sdk.getNewDerivation(self.identifier, "M/" + self.keyIndex + "'/" + chainIdx)
731
            .then(function(newDerivation) {
732
                var path = newDerivation.path;
733
                var addressFromServer = newDerivation.address;
734
                var decodedFromServer;
735
736
                try {
737
                    // Decode the address the serer gave us
738
                    decodedFromServer = self.decodeAddress(addressFromServer);
739
                    if ("cashAddrPrefix" in self.network && self.useNewCashAddr && decodedFromServer.type === "base58") {
740
                        self.bypassNewAddressCheck = false;
741
                    }
742
                } catch (e) {
743
                    throw new blocktrail.WalletAddressError("Failed to decode address [" + newDerivation.address + "]");
744
                }
745
746
                if (!self.bypassNewAddressCheck) {
747
                    // We need to reproduce this address with the same path,
748
                    // but the server (for BCH cashaddrs) uses base58?
749
                    var verifyAddress = self.getAddressByPath(newDerivation.path);
750
751
                    // If this occasion arises:
752
                    if ("cashAddrPrefix" in self.network && self.useNewCashAddr && decodedFromServer.type === "base58") {
753
                        // Decode our the address we produced for the path
754
                        var decodeOurs;
755
                        try {
756
                            decodeOurs = self.decodeAddress(verifyAddress);
757
                        } catch (e) {
758
                            throw new blocktrail.WalletAddressError("Error while verifying address from server [" + e.message + "]");
759
                        }
760
761
                        // Peek beyond the encoding - the hashes must match at least
762
                        if (decodeOurs.decoded.hash.toString('hex') !== decodedFromServer.decoded.hash.toString('hex')) {
763
                            throw new blocktrail.WalletAddressError("Failed to verify legacy address [hash mismatch]");
764
                        }
765
766
                        var matchedP2PKH = decodeOurs.decoded.version === bitcoin.script.types.P2PKH &&
767
                            decodedFromServer.decoded.version === self.network.pubKeyHash;
768
                        var matchedP2SH = decodeOurs.decoded.version === bitcoin.script.types.P2SH &&
769
                            decodedFromServer.decoded.version === self.network.scriptHash;
770
771
                        if (!(matchedP2PKH || matchedP2SH)) {
772
                            throw new blocktrail.WalletAddressError("Failed to verify legacy address [prefix mismatch]");
773
                        }
774
775
                        // We are satisfied that the address is for the same
776
                        // destination, so substitute addressFromServer with our
777
                        // 'reencoded' form.
778
                        addressFromServer = decodeOurs.address;
779
                    }
780
781
                    // debug check
782
                    if (verifyAddress !== addressFromServer) {
783
                        throw new blocktrail.WalletAddressError("Failed to verify address [" + newDerivation.address + "] !== [" + addressFromServer + "]");
784
                    }
785
                }
786
787
                return [addressFromServer, path];
788
            })
789
    );
790
791
    return deferred.promise;
792
};
793
794
/**
795
 * get the balance for the wallet
796
 *
797
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
798
 * @returns {q.Promise}
799
 */
800
Wallet.prototype.getBalance = function(cb) {
801
    var self = this;
802
803
    var deferred = q.defer();
804
    deferred.promise.spreadNodeify(cb);
805
806
    deferred.resolve(
807
        self.sdk.getWalletBalance(self.identifier)
808
            .then(function(result) {
809
                return [result.confirmed, result.unconfirmed];
810
            })
811
    );
812
813
    return deferred.promise;
814
};
815
816
/**
817
 * get the balance for the wallet
818
 *
819
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
820
 * @returns {q.Promise}
821
 */
822
Wallet.prototype.getInfo = function(cb) {
823
    var self = this;
824
825
    var deferred = q.defer();
826
    deferred.promise.spreadNodeify(cb);
827
828
    deferred.resolve(
829
        self.sdk.getWalletBalance(self.identifier)
830
    );
831
832
    return deferred.promise;
833
};
834
835
/**
836
 *
837
 * @param [force]   bool            ignore warnings (such as non-zero balance)
838
 * @param [cb]      function        callback(err, success)
839
 * @returns {q.Promise}
840
 */
841
Wallet.prototype.deleteWallet = function(force, cb) {
842
    var self = this;
843
844
    if (typeof force === "function") {
845
        cb = force;
846
        force = false;
847
    }
848
849
    var deferred = q.defer();
850
    deferred.promise.nodeify(cb);
851
852
    if (self.locked) {
853
        deferred.reject(new blocktrail.WalletDeleteError("Wallet needs to be unlocked to delete wallet"));
854
        return deferred.promise;
855
    }
856
857
    var checksum = self.primaryPrivateKey.getAddress();
858
    var privBuf = self.primaryPrivateKey.keyPair.d.toBuffer(32);
859
    var signature = bitcoinMessage.sign(checksum, self.network.messagePrefix, privBuf, true).toString('base64');
860
861
    deferred.resolve(
862
        self.sdk.deleteWallet(self.identifier, checksum, signature, force)
863
            .then(function(result) {
864
                return result.deleted;
865
            })
866
    );
867
868
    return deferred.promise;
869
};
870
871
/**
872
 * create, sign and send a transaction
873
 *
874
 * @param pay                   array       {'address': (int)value}     coins to send
875
 * @param [changeAddress]       bool        change address to use (auto generated if NULL)
876
 * @param [allowZeroConf]       bool        allow zero confirmation unspent outputs to be used in coin selection
877
 * @param [randomizeChangeIdx]  bool        randomize the index of the change output (default TRUE, only disable if you have a good reason to)
878
 * @param [feeStrategy]         string      defaults to Wallet.FEE_STRATEGY_OPTIMAL
879
 * @param [twoFactorToken]      string      2FA token
880
 * @param options
881
 * @param [cb]                  function    callback(err, txHash)
882
 * @returns {q.Promise}
883
 */
884
Wallet.prototype.pay = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, twoFactorToken, options, cb) {
885
886
    /* jshint -W071 */
887
    var self = this;
888
889 View Code Duplication
    if (typeof changeAddress === "function") {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
890
        cb = changeAddress;
891
        changeAddress = null;
892
    } else if (typeof allowZeroConf === "function") {
893
        cb = allowZeroConf;
894
        allowZeroConf = false;
895
    } else if (typeof randomizeChangeIdx === "function") {
896
        cb = randomizeChangeIdx;
897
        randomizeChangeIdx = true;
898
    } else if (typeof feeStrategy === "function") {
899
        cb = feeStrategy;
900
        feeStrategy = null;
901
    } else if (typeof twoFactorToken === "function") {
902
        cb = twoFactorToken;
903
        twoFactorToken = null;
904
    } else if (typeof options === "function") {
905
        cb = options;
906
        options = {};
907
    }
908
909
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
910
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
911
    options = options || {};
912
    var checkFee = typeof options.checkFee !== "undefined" ? options.checkFee : true;
913
914
    var deferred = q.defer();
915
    deferred.promise.nodeify(cb);
916
917
    if (self.locked) {
918
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to send coins"));
919
        return deferred.promise;
920
    }
921
922
    q.nextTick(function() {
923
        deferred.notify(Wallet.PAY_PROGRESS_START);
924
        self.buildTransaction(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options)
925
            .then(
926
            function(r) { return r; },
927
            function(e) { deferred.reject(e); },
928
            function(progress) {
929
                deferred.notify(progress);
930
            }
931
        )
932
            .spread(
933
            function(tx, utxos) {
934
935
                deferred.notify(Wallet.PAY_PROGRESS_SEND);
936
937
                var data = {
938
                    signed_transaction: tx.toHex(),
939
                    base_transaction: tx.__toBuffer(null, null, false).toString('hex')
940
                };
941
942
                return self.sendTransaction(data, utxos.map(function(utxo) { return utxo['path']; }), checkFee, twoFactorToken, options.prioboost)
943
                    .then(function(result) {
944
                        deferred.notify(Wallet.PAY_PROGRESS_DONE);
945
946
                        if (!result || !result['complete'] || result['complete'] === 'false') {
947
                            deferred.reject(new blocktrail.TransactionSignError("Failed to completely sign transaction"));
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
948
                        } else {
949
                            return result['txid'];
950
                        }
951
                    });
952
            },
953
            function(e) {
954
                throw e;
955
            }
956
        )
957
            .then(
958
            function(r) { deferred.resolve(r); },
959
            function(e) { deferred.reject(e); }
960
        )
961
        ;
962
    });
963
964
    return deferred.promise;
965
};
966
967
Wallet.prototype.decodeAddress = function(address) {
968
    return Wallet.getAddressAndType(address, this.network, this.useNewCashAddr);
969
};
970
971
function readBech32Address(address, network) {
972
    var addr;
973
    var err;
974
    try {
975
        addr = bitcoin.address.fromBech32(address, network);
976
        err = null;
977
978
    } catch (_err) {
979
        err = _err;
980
    }
981
982
    if (!err) {
983
        // Valid bech32 but invalid network immediately alerts
984
        if (addr.prefix !== network.bech32) {
985
            throw new blocktrail.InvalidAddressError("Address invalid on this network");
986
        }
987
    }
988
989
    return [err, addr];
990
}
991
992
function readCashAddress(address, network) {
993
    var addr;
994
    var err;
995
    address = address.toLowerCase();
996
    try {
997
        addr = bitcoin.address.fromCashAddress(address);
998
        err = null;
999
    } catch (_err) {
1000
        err = _err;
1001
    }
1002
1003
    if (err) {
1004
        try {
1005
            addr = bitcoin.address.fromCashAddress(network.cashAddrPrefix + ':' + address);
1006
            err = null;
1007
        } catch (_err) {
1008
            err = _err;
1009
        }
1010
    }
1011
1012
    if (!err) {
1013
        // Valid base58 but invalid network immediately alerts
1014
        if (addr.prefix !== network.cashAddrPrefix) {
1015
            throw new Error(address + ' has an invalid prefix');
1016
        }
1017
    }
1018
1019
    return [err, addr];
1020
}
1021
1022
function readBase58Address(address, network) {
1023
    var addr;
1024
    var err;
1025
    try {
1026
        addr = bitcoin.address.fromBase58Check(address);
1027
        err = null;
1028
    } catch (_err) {
1029
        err = _err;
1030
    }
1031
1032
    if (!err) {
1033
        // Valid base58 but invalid network immediately alerts
1034
        if (addr.version !== network.pubKeyHash && addr.version !== network.scriptHash) {
1035
            throw new blocktrail.InvalidAddressError("Address invalid on this network");
1036
        }
1037
    }
1038
1039
    return [err, addr];
1040
}
1041
1042
Wallet.getAddressAndType = function(address, network, allowCashAddress) {
1043
    var addr;
1044
    var type;
1045
    var err;
1046
1047
    function readAddress(reader, readType) {
1048
        var decoded = reader(address, network);
1049
        if (decoded[0] === null) {
1050
            addr = decoded[1];
1051
            type = readType;
1052
        } else {
1053
            err = decoded[0];
1054
        }
1055
    }
1056
1057
    if (network === bitcoin.networks.bitcoin ||
1058
        network === bitcoin.networks.testnet ||
1059
        network === bitcoin.networks.regtest
1060
    ) {
1061
        readAddress(readBech32Address, "bech32");
1062
    }
1063
1064
    if (!addr && 'cashAddrPrefix' in network && allowCashAddress) {
1065
        readAddress(readCashAddress, "cashaddr");
1066
    }
1067
1068
    if (!addr) {
1069
        readAddress(readBase58Address, "base58");
1070
    }
1071
1072
    if (addr) {
1073
        return {
1074
            address: address,
1075
            decoded: addr,
1076
            type: type
1077
        };
1078
    } else {
1079
        throw new blocktrail.InvalidAddressError(err.message);
1080
    }
1081
};
1082
1083
Wallet.convertPayToOutputs = function(pay, network, allowCashAddr) {
1084
    var send = [];
1085
1086
    var readFunc;
1087
1088
    // Deal with two different forms
1089
    if (Array.isArray(pay)) {
1090
        // output[]
1091
        readFunc = function(i, output, obj) {
1092
            if (typeof output !== "object") {
1093
                throw new Error("Invalid transaction output for numerically indexed list [1]");
1094
            }
1095
1096
            var keys = Object.keys(output);
1097
            if (keys.indexOf("scriptPubKey") !== -1 && keys.indexOf("value") !== -1) {
1098
                obj.scriptPubKey = output["scriptPubKey"];
1099
                obj.value = output["value"];
1100
            } else if (keys.indexOf("address") !== -1 && keys.indexOf("value") !== -1) {
1101
                obj.address = output["address"];
1102
                obj.value = output["value"];
1103
            } else if (keys.length === 2 && output.length === 2 && keys[0] === '0' && keys[1] === '1') {
1104
                obj.address = output[0];
1105
                obj.value = output[1];
1106
            } else {
1107
                throw new Error("Invalid transaction output for numerically indexed list [2]");
1108
            }
1109
        };
1110
    } else if (typeof pay === "object") {
1111
        // map[addr]amount
1112
        readFunc = function(address, value, obj) {
1113
            obj.address = address.trim();
1114
            obj.value = value;
1115
            if (obj.address === Wallet.OP_RETURN) {
1116
                var datachunk = Buffer.isBuffer(value) ? value : new Buffer(value, 'utf-8');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1117
                obj.scriptPubKey = bitcoin.script.nullData.output.encode(datachunk).toString('hex');
1118
                obj.value = 0;
1119
                obj.address = null;
1120
            }
1121
        };
1122
    } else {
1123
        throw new Error("Invalid input");
1124
    }
1125
1126
    Object.keys(pay).forEach(function(key) {
1127
        var obj = {};
1128
        readFunc(key, pay[key], obj);
1129
1130
        if (parseInt(obj.value, 10).toString() !== obj.value.toString()) {
1131
            throw new blocktrail.WalletSendError("Values should be in Satoshis");
1132
        }
1133
1134
        // Remove address, replace with scriptPubKey
1135
        if (typeof obj.address === "string") {
1136
            try {
1137
                var addrAndType = Wallet.getAddressAndType(obj.address, network, allowCashAddr);
1138
                obj.scriptPubKey = bitcoin.address.toOutputScript(addrAndType.address, network, allowCashAddr).toString('hex');
1139
                delete obj.address;
1140
            } catch (e) {
1141
                throw new blocktrail.InvalidAddressError("Invalid address [" + obj.address + "] (" + e.message + ")");
1142
            }
1143
        }
1144
1145
        // Extra checks when the output isn't OP_RETURN
1146
        if (obj.scriptPubKey.slice(0, 2) !== "6a") {
1147
            if (!(obj.value = parseInt(obj.value, 10))) {
1148
                throw new blocktrail.WalletSendError("Values should be non zero");
1149
            } else if (obj.value <= blocktrail.DUST) {
1150
                throw new blocktrail.WalletSendError("Values should be more than dust (" + blocktrail.DUST + ")");
1151
            }
1152
        }
1153
1154
        // Value fully checked now
1155
        obj.value = parseInt(obj.value, 10);
1156
1157
        send.push(obj);
1158
    });
1159
1160
    return send;
1161
};
1162
1163
Wallet.prototype.buildTransaction = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options, cb) {
1164
    /* jshint -W071 */
1165
    var self = this;
1166
1167 View Code Duplication
    if (typeof changeAddress === "function") {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1168
        cb = changeAddress;
1169
        changeAddress = null;
1170
    } else if (typeof allowZeroConf === "function") {
1171
        cb = allowZeroConf;
1172
        allowZeroConf = false;
1173
    } else if (typeof randomizeChangeIdx === "function") {
1174
        cb = randomizeChangeIdx;
1175
        randomizeChangeIdx = true;
1176
    } else if (typeof feeStrategy === "function") {
1177
        cb = feeStrategy;
1178
        feeStrategy = null;
1179
    } else if (typeof options === "function") {
1180
        cb = options;
1181
        options = {};
1182
    }
1183
1184
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
1185
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1186
    options = options || {};
1187
1188
    var deferred = q.defer();
1189
    deferred.promise.spreadNodeify(cb);
1190
1191
    q.nextTick(function() {
1192
        var send;
1193
        try {
1194
            send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr);
1195
        } catch (e) {
1196
            deferred.reject(e);
1197
            return deferred.promise;
1198
        }
1199
1200
        if (!send.length) {
1201
            deferred.reject(new blocktrail.WalletSendError("Need at least one recipient"));
1202
            return deferred.promise;
1203
        }
1204
1205
        deferred.notify(Wallet.PAY_PROGRESS_COIN_SELECTION);
1206
1207
        deferred.resolve(
1208
            self.coinSelection(send, true, allowZeroConf, feeStrategy, options)
1209
            /**
1210
             *
1211
             * @param {Object[]} utxos
1212
             * @param fee
1213
             * @param change
1214
             * @param randomizeChangeIdx
0 ignored issues
show
Documentation introduced by
The parameter randomizeChangeIdx does not exist. Did you maybe forget to remove this comment?
Loading history...
1215
             * @returns {*}
1216
             */
1217
                .spread(function(utxos, fee, change) {
1218
                    var tx, txb, outputs = [];
1219
1220
                    var deferred = q.defer();
1221
1222
                    async.waterfall([
1223
                        /**
1224
                         * prepare
1225
                         *
1226
                         * @param cb
1227
                         */
1228
                        function(cb) {
1229
                            var inputsTotal = utxos.map(function(utxo) {
1230
                                return utxo['value'];
1231
                            }).reduce(function(a, b) {
1232
                                return a + b;
1233
                            });
1234
                            var outputsTotal = send.map(function(output) {
1235
                                return output.value;
1236
                            }).reduce(function(a, b) {
1237
                                return a + b;
1238
                            });
1239
                            var estimatedChange = inputsTotal - outputsTotal - fee;
1240
1241
                            if (estimatedChange > blocktrail.DUST * 2 && estimatedChange !== change) {
1242
                                return cb(new blocktrail.WalletFeeError("the amount of change (" + change + ") " +
1243
                                    "suggested by the coin selection seems incorrect (" + estimatedChange + ")"));
1244
                            }
1245
1246
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1247
                        },
1248
                        /**
1249
                         * init transaction builder
1250
                         *
1251
                         * @param cb
1252
                         */
1253
                        function(cb) {
1254
                            txb = new bitcoin.TransactionBuilder(self.network);
1255
                            if (self.bitcoinCash) {
1256
                                txb.enableBitcoinCash();
1257
                            }
1258
1259
                            cb();
1260
                        },
1261
                        /**
1262
                         * add UTXOs as inputs
1263
                         *
1264
                         * @param cb
1265
                         */
1266
                        function(cb) {
1267
                            var i;
1268
1269
                            for (i = 0; i < utxos.length; i++) {
1270
                                txb.addInput(utxos[i]['hash'], utxos[i]['idx']);
1271
                            }
1272
1273
                            cb();
1274
                        },
1275
                        /**
1276
                         * build desired outputs
1277
                         *
1278
                         * @param cb
1279
                         */
1280
                        function(cb) {
1281
                            send.forEach(function(_send) {
1282
                                if (_send.scriptPubKey) {
1283
                                    outputs.push({scriptPubKey: new Buffer(_send.scriptPubKey, 'hex'), value: _send.value});
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1284
                                } else {
1285
                                    throw new Error("Invalid send");
1286
                                }
1287
                            });
1288
                            cb();
1289
                        },
1290
                        /**
1291
                         * get change address if required
1292
                         *
1293
                         * @param cb
1294
                         */
1295
                        function(cb) {
1296
                            if (change > 0) {
1297
                                if (change <= blocktrail.DUST) {
1298
                                    change = 0; // don't do a change output if it would be a dust output
1299
1300
                                } else {
1301
                                    if (!changeAddress) {
1302
                                        deferred.notify(Wallet.PAY_PROGRESS_CHANGE_ADDRESS);
1303
1304
                                        return self.getNewAddress(self.changeChain, function(err, address) {
1305
                                            if (err) {
1306
                                                return cb(err);
1307
                                            }
1308
                                            changeAddress = address;
1309
                                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1310
                                        });
1311
                                    }
1312
                                }
1313
                            }
1314
1315
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1316
                        },
1317
                        /**
1318
                         * add change to outputs
1319
                         *
1320
                         * @param cb
1321
                         */
1322
                        function(cb) {
1323
                            if (change > 0) {
1324
                                var changeOutput = {
1325
                                    scriptPubKey: bitcoin.address.toOutputScript(changeAddress, self.network, self.useNewCashAddr),
1326
                                    value: change
1327
                                };
1328
                                if (randomizeChangeIdx) {
1329
                                    outputs.splice(_.random(0, outputs.length), 0, changeOutput);
1330
                                } else {
1331
                                    outputs.push(changeOutput);
1332
                                }
1333
                            }
1334
1335
                            cb();
1336
                        },
1337
                        /**
1338
                         * add outputs to txb
1339
                         *
1340
                         * @param cb
1341
                         */
1342
                        function(cb) {
1343
                            outputs.forEach(function(outputInfo) {
1344
                                txb.addOutput(outputInfo.scriptPubKey, outputInfo.value);
1345
                            });
1346
1347
                            cb();
1348
                        },
1349
                        /**
1350
                         * sign
1351
                         *
1352
                         * @param cb
1353
                         */
1354
                        function(cb) {
1355
                            var i, privKey, path, redeemScript, witnessScript;
1356
1357
                            deferred.notify(Wallet.PAY_PROGRESS_SIGN);
1358
1359
                            for (i = 0; i < utxos.length; i++) {
1360
                                var mode = SignMode.SIGN;
1361
                                if (utxos[i].sign_mode) {
1362
                                    mode = utxos[i].sign_mode;
1363
                                }
1364
1365
                                redeemScript = null;
0 ignored issues
show
Unused Code introduced by
The assignment to redeemScript seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
1366
                                witnessScript = null;
1367
                                if (mode === SignMode.SIGN) {
1368
                                    path = utxos[i]['path'].replace("M", "m");
1369
1370
                                    // todo: regenerate scripts for path and compare for utxo (paranoid mode)
1371
                                    if (self.primaryPrivateKey) {
1372
                                        privKey = Wallet.deriveByPath(self.primaryPrivateKey, path, "m").keyPair;
1373
                                    } else if (self.backupPrivateKey) {
1374
                                        privKey = Wallet.deriveByPath(self.backupPrivateKey, path.replace(/^m\/(\d+)\'/, 'm/$1'), "m").keyPair;
1375
                                    } else {
1376
                                        throw new Error("No master privateKey present");
1377
                                    }
1378
1379
                                    redeemScript = new Buffer(utxos[i]['redeem_script'], 'hex');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1380
                                    if (typeof utxos[i]['witness_script'] === 'string') {
1381
                                        witnessScript = new Buffer(utxos[i]['witness_script'], 'hex');
1382
                                    }
1383
1384
                                    var sigHash = bitcoin.Transaction.SIGHASH_ALL;
1385
                                    if (self.bitcoinCash) {
1386
                                        sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143;
1387
                                    }
1388
1389
                                    txb.sign(i, privKey, redeemScript, sigHash, utxos[i].value, witnessScript);
1390
                                }
1391
                            }
1392
1393
                            tx = txb.buildIncomplete();
1394
1395
                            cb();
1396
                        },
1397
                        /**
1398
                         * estimate fee to verify that the API is not providing us wrong data
1399
                         *
1400
                         * @param cb
1401
                         */
1402
                        function(cb) {
1403
                            var estimatedFee = Wallet.estimateVsizeFee(tx, utxos);
1404
1405
                            if (self.sdk.feeSanityCheck) {
1406
                                switch (feeStrategy) {
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
1407
                                    case Wallet.FEE_STRATEGY_BASE_FEE:
0 ignored issues
show
Bug introduced by
The variable Wallet seems to be never declared. If this is a global, consider adding a /** global: Wallet */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1408
                                        if (Math.abs(estimatedFee - fee) > blocktrail.BASE_FEE) {
1409
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1410
                                                "seems incorrect (" + estimatedFee + ") for FEE_STRATEGY_BASE_FEE"));
1411
                                        }
1412
                                    break;
1413
1414
                                    case Wallet.FEE_STRATEGY_HIGH_PRIORITY:
1415
                                    case Wallet.FEE_STRATEGY_OPTIMAL:
1416
                                    case Wallet.FEE_STRATEGY_LOW_PRIORITY:
1417
                                        if (fee > estimatedFee * self.feeSanityCheckBaseFeeMultiplier) {
1418
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1419
                                                "seems awefully high (" + estimatedFee + ") for FEE_STRATEGY_OPTIMAL"));
1420
                                        }
1421
                                    break;
1422
                                }
1423
                            }
1424
1425
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1426
                        }
1427
                    ], function(err) {
1428
                        if (err) {
1429
                            deferred.reject(new blocktrail.WalletSendError(err));
1430
                            return;
1431
                        }
1432
1433
                        deferred.resolve([tx, utxos]);
1434
                    });
1435
1436
                    return deferred.promise;
1437
                }
1438
            )
1439
        );
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1440
    });
1441
1442
    return deferred.promise;
1443
};
1444
1445
1446
/**
1447
 * use the API to get the best inputs to use based on the outputs
1448
 *
1449
 * @param pay               array       {'address': (int)value}     coins to send
1450
 * @param [lockUTXO]        bool        lock UTXOs for a few seconds to allow for transaction to be created
1451
 * @param [allowZeroConf]   bool        allow zero confirmation unspent outputs to be used in coin selection
1452
 * @param [feeStrategy]     string      defaults to FEE_STRATEGY_OPTIMAL
1453
 * @param [options]         object
1454
 * @param [cb]              function    callback(err, utxos, fee, change)
1455
 * @returns {q.Promise}
1456
 */
1457
Wallet.prototype.coinSelection = function(pay, lockUTXO, allowZeroConf, feeStrategy, options, cb) {
1458
    var self = this;
1459
1460
    if (typeof lockUTXO === "function") {
1461
        cb = lockUTXO;
1462
        lockUTXO = true;
1463
    } else if (typeof allowZeroConf === "function") {
1464
        cb = allowZeroConf;
1465
        allowZeroConf = false;
1466
    } else if (typeof feeStrategy === "function") {
1467
        cb = feeStrategy;
1468
        feeStrategy = null;
1469
    } else if (typeof options === "function") {
1470
        cb = options;
1471
        options = {};
1472
    }
1473
1474
    lockUTXO = typeof lockUTXO !== "undefined" ? lockUTXO : true;
1475
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1476
    options = options || {};
1477
1478
    var send;
1479
    try {
1480
        send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr);
1481
    } catch (e) {
1482
        var deferred = q.defer();
1483
        deferred.promise.nodeify(cb);
1484
        deferred.reject(e);
1485
        return deferred.promise;
1486
    }
1487
1488
    return self.sdk.coinSelection(self.identifier, send, lockUTXO, allowZeroConf, feeStrategy, options, cb);
1489
};
1490
1491
/**
1492
 * send the transaction using the API
1493
 *
1494
 * @param txHex             string      partially signed transaction as hex string
1495
 * @param paths             array       list of paths used in inputs which should be cosigned by the API
1496
 * @param checkFee          bool        when TRUE the API will verify if the fee is 100% correct and otherwise throw an exception
1497
 * @param [twoFactorToken]  string      2FA token
1498
 * @param prioboost         bool
1499
 * @param [cb]              function    callback(err, txHash)
1500
 * @returns {q.Promise}
1501
 */
1502
Wallet.prototype.sendTransaction = function(txHex, paths, checkFee, twoFactorToken, prioboost, cb) {
1503
    var self = this;
1504
1505
    if (typeof twoFactorToken === "function") {
1506
        cb = twoFactorToken;
1507
        twoFactorToken = null;
1508
        prioboost = false;
1509
    } else if (typeof prioboost === "function") {
1510
        cb = twoFactorToken;
1511
        prioboost = false;
1512
    }
1513
1514
    var deferred = q.defer();
1515
    deferred.promise.nodeify(cb);
1516
1517
    self.sdk.sendTransaction(self.identifier, txHex, paths, checkFee, twoFactorToken, prioboost)
1518
        .then(
1519
            function(result) {
1520
                deferred.resolve(result);
1521
            },
1522
            function(e) {
1523
                if (e.requires_2fa) {
1524
                    deferred.reject(new blocktrail.WalletMissing2FAError());
1525
                } else if (e.message.match(/Invalid two_factor_token/)) {
1526
                    deferred.reject(new blocktrail.WalletInvalid2FAError());
1527
                } else {
1528
                    deferred.reject(e);
1529
                }
1530
            }
1531
        )
1532
    ;
1533
1534
    return deferred.promise;
1535
};
1536
1537
/**
1538
 * setup a webhook for this wallet
1539
 *
1540
 * @param url           string      URL to receive webhook events
1541
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1542
 * @param [cb]          function    callback(err, webhook)
1543
 * @returns {q.Promise}
1544
 */
1545
Wallet.prototype.setupWebhook = function(url, identifier, cb) {
1546
    var self = this;
1547
1548
    if (typeof identifier === "function") {
1549
        cb = identifier;
1550
        identifier = null;
1551
    }
1552
1553
    identifier = identifier || ('WALLET-' + self.identifier);
1554
1555
    return self.sdk.setupWalletWebhook(self.identifier, identifier, url, cb);
1556
};
1557
1558
/**
1559
 * delete a webhook that was created for this wallet
1560
 *
1561
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1562
 * @param [cb]          function    callback(err, success)
1563
 * @returns {q.Promise}
1564
 */
1565
Wallet.prototype.deleteWebhook = function(identifier, cb) {
1566
    var self = this;
1567
1568
    if (typeof identifier === "function") {
1569
        cb = identifier;
1570
        identifier = null;
1571
    }
1572
1573
    identifier = identifier || ('WALLET-' + self.identifier);
1574
1575
    return self.sdk.deleteWalletWebhook(self.identifier, identifier, cb);
1576
};
1577
1578
/**
1579
 * get all transactions for the wallet (paginated)
1580
 *
1581
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1582
 * @param [cb]      function    callback(err, transactions)
1583
 * @returns {q.Promise}
1584
 */
1585
Wallet.prototype.transactions = function(params, cb) {
1586
    var self = this;
1587
1588
    return self.sdk.walletTransactions(self.identifier, params, cb);
1589
};
1590
1591
Wallet.prototype.maxSpendable = function(allowZeroConf, feeStrategy, options, cb) {
1592
    var self = this;
1593
1594
    if (typeof allowZeroConf === "function") {
1595
        cb = allowZeroConf;
1596
        allowZeroConf = false;
1597
    } else if (typeof feeStrategy === "function") {
1598
        cb = feeStrategy;
1599
        feeStrategy = null;
1600
    } else if (typeof options === "function") {
1601
        cb = options;
1602
        options = {};
1603
    }
1604
1605
    if (typeof allowZeroConf === "object") {
1606
        options = allowZeroConf;
1607
        allowZeroConf = false;
1608
    } else if (typeof feeStrategy === "object") {
1609
        options = feeStrategy;
1610
        feeStrategy = null;
1611
    }
1612
1613
    options = options || {};
1614
1615
    if (typeof options.allowZeroConf !== "undefined") {
1616
        allowZeroConf = options.allowZeroConf;
1617
    }
1618
    if (typeof options.feeStrategy !== "undefined") {
1619
        feeStrategy = options.feeStrategy;
1620
    }
1621
1622
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1623
1624
    return self.sdk.walletMaxSpendable(self.identifier, allowZeroConf, feeStrategy, options, cb);
1625
};
1626
1627
/**
1628
 * get all addresses for the wallet (paginated)
1629
 *
1630
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1631
 * @param [cb]      function    callback(err, addresses)
1632
 * @returns {q.Promise}
1633
 */
1634
Wallet.prototype.addresses = function(params, cb) {
1635
    var self = this;
1636
1637
    return self.sdk.walletAddresses(self.identifier, params, cb);
1638
};
1639
1640
/**
1641
 * @param address   string      the address to label
1642
 * @param label     string      the label
1643
 * @param [cb]      function    callback(err, res)
1644
 * @returns {q.Promise}
1645
 */
1646
Wallet.prototype.labelAddress = function(address, label, cb) {
1647
    var self = this;
1648
1649
    return self.sdk.labelWalletAddress(self.identifier, address, label, cb);
1650
};
1651
1652
/**
1653
 * get all UTXOs for the wallet (paginated)
1654
 *
1655
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1656
 * @param [cb]      function    callback(err, addresses)
1657
 * @returns {q.Promise}
1658
 */
1659
Wallet.prototype.utxos = function(params, cb) {
1660
    var self = this;
1661
1662
    return self.sdk.walletUTXOs(self.identifier, params, cb);
1663
};
1664
1665
Wallet.prototype.unspentOutputs = Wallet.prototype.utxos;
1666
1667
/**
1668
 * sort list of pubkeys to be used in a multisig redeemscript
1669
 *  sorted in lexicographical order on the hex of the pubkey
1670
 *
1671
 * @param pubKeys   {bitcoin.HDNode[]}
1672
 * @returns string[]
1673
 */
1674
Wallet.sortMultiSigKeys = function(pubKeys) {
1675
    pubKeys.sort(function(key1, key2) {
1676
        return key1.toString('hex').localeCompare(key2.toString('hex'));
1677
    });
1678
1679
    return pubKeys;
1680
};
1681
1682
/**
1683
 * determine how much fee is required based on the inputs and outputs
1684
 *  this is an estimation, not a proper 100% correct calculation
1685
 *
1686
 * @todo: mark deprecated in favor of estimations where UTXOS are known
1687
 * @param {bitcoin.Transaction} tx
1688
 * @param {int} feePerKb when not null use this feePerKb, otherwise use BASE_FEE legacy calculation
1689
 * @returns {number}
1690
 */
1691
Wallet.estimateIncompleteTxFee = function(tx, feePerKb) {
1692
    var size = Wallet.estimateIncompleteTxSize(tx);
1693
    var sizeKB = size / 1000;
1694
    var sizeKBCeil = Math.ceil(size / 1000);
1695
1696
    if (feePerKb) {
1697
        return parseInt(sizeKB * feePerKb, 10);
1698
    } else {
1699
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1700
    }
1701
};
1702
1703
/**
1704
 * Takes tx and utxos, computing their estimated vsize,
1705
 * and uses feePerKb (or BASEFEE as default) to estimate
1706
 * the number of satoshis in fee.
1707
 *
1708
 * @param {bitcoin.Transaction} tx
1709
 * @param {Array} utxos
1710
 * @param feePerKb
1711
 * @returns {Number}
1712
 */
1713
Wallet.estimateVsizeFee = function(tx, utxos, feePerKb) {
1714
    var vsize = SizeEstimation.estimateTxVsize(tx, utxos);
1715
    var sizeKB = vsize / 1000;
1716
    var sizeKBCeil = Math.ceil(vsize / 1000);
1717
1718
    if (feePerKb) {
1719
        return parseInt(sizeKB * feePerKb, 10);
1720
    } else {
1721
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1722
    }
1723
};
1724
1725
/**
1726
 * determine how much fee is required based on the inputs and outputs
1727
 *  this is an estimation, not a proper 100% correct calculation
1728
 *
1729
 * @param {bitcoin.Transaction} tx
1730
 * @returns {number}
1731
 */
1732
Wallet.estimateIncompleteTxSize = function(tx) {
1733
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1734
1735
    size += tx.outs.length * 34;
1736
1737
    tx.ins.forEach(function(txin) {
1738
        var scriptSig = txin.script,
1739
            scriptType = bitcoin.script.classifyInput(scriptSig);
1740
1741
        var multiSig = [2, 3]; // tmp hardcoded
1742
1743
        // Re-classify if P2SH
1744
        if (!multiSig && scriptType === 'scripthash') {
1745
            var sigChunks = bitcoin.script.decompile(scriptSig);
1746
            var redeemScript = sigChunks.slice(-1)[0];
1747
            scriptSig = bitcoin.script.compile(sigChunks.slice(0, -1));
1748
            scriptType = bitcoin.script.classifyInput(scriptSig);
1749
1750
            if (bitcoin.script.classifyOutput(redeemScript) !== scriptType) {
1751
                throw new blocktrail.TransactionInputError('Non-matching scriptSig and scriptPubKey in input');
1752
            }
1753
1754
            // figure out M of N for multisig (code from internal usage of bitcoinjs)
1755
            if (scriptType === 'multisig') {
1756
                var rsChunks = bitcoin.script.decompile(redeemScript);
1757
                var mOp = rsChunks[0];
1758
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1759
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1760
                }
1761
1762
                var nOp = rsChunks[redeemScript.chunks.length - 2];
1763
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1764
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1765
                }
1766
1767
                var m = mOp - (bitcoin.opcodes.OP_1 - 1);
1768
                var n = nOp - (bitcoin.opcodes.OP_1 - 1);
1769
                if (n < m) {
1770
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1771
                }
1772
1773
                multiSig = [m, n];
1774
            }
1775
        }
1776
1777
        if (multiSig) {
1778
            size += (
1779
                32 + // txhash
1780
                4 + // idx
1781
                3 + // scriptVarInt[>=253]
1782
                1 + // OP_0
1783
                ((1 + 72) * multiSig[0]) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1784
                (2 + 105) + // OP_PUSHDATA[>=75] + script
1785
                4 // sequence
1786
            );
1787
1788
        } else {
1789
            size += 32 + // txhash
1790
                4 + // idx
1791
                73 + // sig
1792
                34 + // script
1793
                4; // sequence
1794
        }
1795
    });
1796
1797
    return size;
1798
};
1799
1800
/**
1801
 * determine how much fee is required based on the amount of inputs and outputs
1802
 *  this is an estimation, not a proper 100% correct calculation
1803
 *  this asumes all inputs are 2of3 multisig
1804
 *
1805
 * @todo: mark deprecated in favor of situations where UTXOS are known
1806
 * @param txinCnt       {number}
1807
 * @param txoutCnt      {number}
1808
 * @returns {number}
1809
 */
1810
Wallet.estimateFee = function(txinCnt, txoutCnt) {
1811
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1812
1813
    size += txoutCnt * 34;
1814
1815
    size += (
1816
            32 + // txhash
1817
            4 + // idx
1818
            3 + // scriptVarInt[>=253]
1819
            1 + // OP_0
1820
            ((1 + 72) * 2) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1821
            (2 + 105) + // OP_PUSHDATA[>=75] + script
1822
            4 // sequence
1823
        ) * txinCnt;
1824
1825
    var sizeKB = Math.ceil(size / 1000);
1826
1827
    return sizeKB * blocktrail.BASE_FEE;
1828
};
1829
1830
/**
1831
 * create derived key from parent key by path
1832
 *
1833
 * @param hdKey     {bitcoin.HDNode}
1834
 * @param path      string
1835
 * @param keyPath   string
1836
 * @returns {bitcoin.HDNode}
1837
 */
1838
Wallet.deriveByPath = function(hdKey, path, keyPath) {
1839
    keyPath = keyPath || (!!hdKey.keyPair.d ? "m" : "M");
1840
1841
    if (path[0].toLowerCase() !== "m" || keyPath[0].toLowerCase() !== "m") {
1842
        throw new blocktrail.KeyPathError("Wallet.deriveByPath only works with absolute paths. (" + path + ", " + keyPath + ")");
1843
    }
1844
1845
    if (path[0] === "m" && keyPath[0] === "M") {
1846
        throw new blocktrail.KeyPathError("Wallet.deriveByPath can't derive private path from public parent. (" + path + ", " + keyPath + ")");
1847
    }
1848
1849
    // if the desired path is public while the input is private
1850
    var toPublic = path[0] === "M" && keyPath[0] === "m";
1851
    if (toPublic) {
1852
        // derive the private path, convert to public when returning
1853
        path[0] = "m";
1854
    }
1855
1856
    // keyPath should be the parent parent of path
1857
    if (path.toLowerCase().indexOf(keyPath.toLowerCase()) !== 0) {
1858
        throw new blocktrail.KeyPathError("Wallet.derivePath requires path (" + path + ") to be a child of keyPath (" + keyPath + ")");
1859
    }
1860
1861
    // remove the part of the path we already have
1862
    path = path.substr(keyPath.length);
1863
1864
    // iterate over the chunks and derive
1865
    var newKey = hdKey;
1866
    path.replace(/^\//, "").split("/").forEach(function(chunk) {
1867
        if (!chunk) {
1868
            return;
1869
        }
1870
1871
        if (chunk.indexOf("'") !== -1) {
1872
            chunk = parseInt(chunk.replace("'", ""), 10) + bitcoin.HDNode.HIGHEST_BIT;
1873
        }
1874
1875
        newKey = newKey.derive(parseInt(chunk, 10));
1876
    });
1877
1878
    if (toPublic) {
1879
        return newKey.neutered();
1880
    } else {
1881
        return newKey;
1882
    }
1883
};
1884
1885
module.exports = Wallet;
1886